package Agent;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import org.rtscup.client.adk.Agent;
import org.rtscup.core.Building;
import org.rtscup.core.Entity;
import org.rtscup.core.Map;
import org.rtscup.core.Obstacle;
import org.rtscup.core.Resource;
import org.rtscup.core.Unit;
import org.rtscup.core.id.TypeId;
import org.rtscup.util.IDGenerator;

import Basic.Point;
import Basic.StaticObstacle;
import Basic.Status;
import Basic.Tiles;
import Game.Blackboard;

public class CopyOfMinerAgent extends Agent {

	private int bestBuilding; // indice do vetor de buildings com o melhor
	// building a ser visitado
	private int bestResource; // indice do vetor de resources com o melhor
	// resource a ser visitado
	private Point nextPoint; // proximo ponto a ser alcancado (necessario pra
	// nao colidir nas paredes)

	// Estrutura Tiles, que da ao agente a visao do mapa em tiles.
	private Tiles tiles;

	// Estado do agente
	private Status status;
	private Status prevStatus; // Apenas um indicador do estado anterior para
	// certas ocasioes

	private Map map; // Mapa do jogo
	private StaticObstacle[] obstacles; // Vetor com todos os obstaculos
	private Resource[] resources; // Vetor com todas as minas
	// minerio ainda esta
	// disponivel nas minas
	// Eh necessario pois na mina nao existe um atributo dizendo quanto minerio
	// tem ainda
	private Building[] buildings; // Vetor com todas as bases (lugar onde
	// deposita a mina)
	private int[] units; // Vetor com os ids de todos os agentes
	private int lastCollectCount;
	private HashMap<Integer, Integer> resourcesMark;

	private List<Point> points;

	// private FileWriter saida;

	/**
	 * Construtor do agente.
	 * 
	 * @param name
	 *            - O nome do agente, condizente com algum nome escrito no
	 *            arquivo game.xml
	 * @param team
	 *            - O time do agente, condizente com algum time do arquivo
	 *            game.xml
	 */
	public CopyOfMinerAgent(String name, String team) {
		super(name, team, IDGenerator.generateID());
		this.status = Status.DECIDING_MINE;

		this.tiles = null;
		this.lastCollectCount = 0;
		this.nextPoint = new Point(0, 0);

		this.points = new ArrayList<Point>();
		// try {
		// saida = new FileWriter(getName() + getId());
		// } catch (IOException e) {
		// e.printStackTrace();
		// }
	}

	/**
	 * Metodo init() - chamado antes do comeco do jogo Aqui eh pra esculhambar
	 * mesmo, pra fazer tudo, pois aqui o jogo ainda nao comecou
	 */
	public void init() {
		Vector<Entity> entities = this.getMemory().getAll().getList(); // Pegar
		// todas as entidades no mapa

		ArrayList<StaticObstacle> obstacles = new ArrayList<StaticObstacle>();
		ArrayList<Resource> resources = new ArrayList<Resource>();
		ArrayList<Building> buildings = new ArrayList<Building>();
		ArrayList<Integer> units = new ArrayList<Integer>();
		ArrayList<Integer> resourcesMark = new ArrayList<Integer>();

		for (int i = 0; i < entities.size(); i++) {
			Entity e = entities.get(i);
			if (e.getType() == TypeId.TYPE_MAP) {
				this.map = (Map) e;
			} else if (e.getType() == TypeId.TYPE_OBSTACLE) {
				Obstacle o = (Obstacle) e;
				StaticObstacle so = new StaticObstacle(o.getX(), o.getY(), o.getWidth(), o.getHeight());
				obstacles.add(so);
			} else if (e.getType() == TypeId.TYPE_RESOURCE) {
				Resource r = (Resource) e;
				StaticObstacle so = new StaticObstacle(r.getX(), r.getY(), r.getWidth(), r.getHeight());
				obstacles.add(so);
				resources.add(r);
				resourcesMark.add(r.getLoad());
			} else if (e.getType() == TypeId.TYPE_BUILDING) {
				Building b = (Building) e;
				StaticObstacle so = new StaticObstacle(b.getX(), b.getY(), b.getWidth(), b.getHeight());
				obstacles.add(so);
				buildings.add(b);
			} else if (e.getType() == TypeId.TYPE_UNIT) {
				units.add(e.getId());
			}
		}

		// Passando para os arrays (melhorar o desempenho)
		this.resources = new Resource[resources.size()];
		this.buildings = new Building[buildings.size()];
		this.obstacles = new StaticObstacle[obstacles.size()];
		this.units = new int[units.size()];
		this.resourcesMark = Blackboard.getInstance(resources).getResourcesMark();
		resources.toArray(this.resources);
		buildings.toArray(this.buildings);
		obstacles.toArray(this.obstacles);
		for (int i = 0; i < units.size(); i++) {
			this.units[i] = units.get(i);
		}

		// Inicializando Tiles
		Unit self = (Unit) this.getSelf();
		// this.tiles = new Tiles(this.map.getWidth(), this.map.getHeight(),
		// this.map.getWidth() / self.getWidth(), this.map.getHeight() /
		// self.getHeight(), this.obstacles);
		//
		// try {
		// saida.write("Comecei em: " + ((Unit) this.getSelf()).getX() + " - " +
		// ((Unit) this.getSelf()).getY() + "\n");
		// } catch (Exception e) {
		// }
	}

	/**
	 * Metodo act() principal. Chamado a cada inicio do tick. Caso o metodo act
	 * nao tenha sido inteiramente executado no final de cada tick, ele eh
	 * interrompido e outro act eh chamado. Porem se ele terminar antes do
	 * termino do tick, o agente nao o chama novamente ate que o novo tick
	 * aconteca.
	 */
	public void act() {
		this.game1();
	}

	/**
	 * Metodo game1() chamado pelo metodo act() que define a estratgegia do
	 * agente minerador para o game1 Mais tarde podemos dividir cada acao em um
	 * metodo diferente. Ex. Um metodo para ir para mina, otro p coletar...
	 */
	public void game1() {

		Unit self = (Unit) this.getSelf();

		Vector<StaticObstacle> removeLater = new Vector<StaticObstacle>();

		for (int i = 0; i < units.length; i++) {
			if (units[i] != self.getId()) {
				Unit u = (Unit) this.getMemory().get(units[i]);
				StaticObstacle so = new StaticObstacle(u.getX(), u.getY(), u.getWidth(), u.getHeight());
				removeLater.add(so);
				this.tiles.addObstacle(so);
			}
		}

		// Inicio da maquina de estados

		if (this.status == Status.DECIDING_MINE) {

			executeStatusDecidingMine(self);

		} else if (this.status == Status.GOING_TO_MINE) {

			executeStatusGoingToMine(self);

		} else if (this.status == Status.COLLECTING) {

			executeStatusCollecting(self);

		} else if (this.status == Status.DECIDING_BUILDING) {

			executeStatusDecidingBuilding(self);

		} else if (this.status == Status.GOING_TO_BUILDING) {

			executeStatusGoingToBuilding(self);

		} else if (this.status == Status.DELIVERING) {

			executeStatusDelivering(self);

		} else if (this.status == Status.WAITING) {

			executeStatusWaiting();

		} else if (this.status == Status.END) {

			// ACTTCATCGACGGCTATCGATTCTCCTTATC

		}

		// Remover as unidades que eu adicionei como obstaculo (para adicionar
		// denovo na proxima posicao)
		for (int i = 0; i < removeLater.size(); i++) {
			this.tiles.removeObstacle(removeLater.get(i));
		}
	}

	private void executeStatusWaiting() {
		if (Math.random() > 0.5) {
			this.status = this.prevStatus;
		}
	}

	private void executeStatusDelivering(Unit self) {
		// Estado onde o agente deposita o minerio

		Building bui = buildings[this.bestBuilding];
		// Se eu ainda tenho minerio e o building nao esta lotado ->
		// continue depositando
		// Caso contrario decida a proxima mina
		if (self.getLoad() > 0 && bui.getLoad() < bui.getLoadMax()) {
			this.deliver(bui.getId());
		} else
			this.status = Status.DECIDING_MINE;
		// Veja que ainda tem erros (ou nao) de logica como -> nao seria
		// melhor eu procurar outro predio
		// Caso eu ainda tenha minerio e o predio atual que eu estou
		// depositando lotou...
		// Esssas questoes filosoficas a gente vai pensar ainda!
	}

	private void executeStatusGoingToBuilding(Unit self) {
		// Estado onde o agente vai para o melhor building depositar o
		// minerio

		Building bui = buildings[this.bestBuilding];
		int selfX = self.getX();
		int selfY = self.getY();

		int buiX = bui.getX();
		int buiY = bui.getY();

		double dist = this.distance(selfX, selfY, buiX, buiY);

		// Caso a minha distancia para o building eh a suficiente para
		// depositar -> DEPOSITE
		// Caso contrario continue andando ate chegar no building
		if (dist <= self.getDeliverRange()) {
			this.status = Status.DELIVERING;
		} else if (bui.getLoad() < bui.getLoadMax()) {
			// Mesma logica do GOING_TO_MINE
			if (this.nextPoint.getX() != selfX || this.nextPoint.getY() != selfY) {
				this.move(this.nextPoint.getX(), this.nextPoint.getY(), self.getSpeed());
			} else {
				goToPlace(self, bui, selfX, selfY, buiX, buiY, Status.GOING_TO_BUILDING);
			}
		} else {
			this.status = Status.DECIDING_BUILDING;
		}
	}

	private void executeStatusDecidingBuilding(Unit self) {
		// Estado onde o agente decide qual o melhor building pra depositar
		// o minerio

		double minDist = Double.MAX_VALUE;
		boolean exist = false;

		for (int b = 0; b < buildings.length; b++) {
			Building bui = buildings[b];
			StaticObstacle so = new StaticObstacle(bui.getX(), bui.getY(), bui.getWidth(), bui.getHeight());
			this.tiles.removeObstacle(so);
			if (bui.getLoad() < bui.getLoadMax()) {
				// Point pBui = new Point(bui.getX(), bui.getY());
				List<Point> path = this.tiles.aStar(bui.getX(), bui.getY(), self.getX(), self.getY());
				if (!path.isEmpty()) {
					double dist = path.size();
					if (dist < minDist) {
						minDist = dist;
						this.bestBuilding = b;
						this.nextPoint = path.get(0);
						exist = true;
					}
				}
			}
			this.tiles.addObstacle(so);
		}

		if (exist) {
			this.status = Status.GOING_TO_BUILDING;
		} else {
			this.prevStatus = Status.DECIDING_BUILDING;
			this.status = Status.WAITING;
		}
	}

	private void executeStatusCollecting(Unit self) {
		// Estado onde p agente coleta minerio

		// Se eu ainda posso carregar mais minerio E a mina ainda tem
		// minerio -> continue minerando
		// Caso contrario va depositar o que voce minerou
		this.resourcesMark.put(this.bestResource, this.resourcesMark.get(this.bestResource) - (self.getLoad() - lastCollectCount));
		if (self.getLoad() < self.getLoadMax() && resourcesMark.get(this.bestResource) > 0) {
			// System.out.println(self.getLoad()+","+self.getLoadMax()+","+this.resourcesMark[this.bestResource]);
			this.collect(this.bestResource);
			lastCollectCount = self.getLoad();
		} else {
			this.status = Status.DECIDING_BUILDING;
			this.lastCollectCount = 0;
		}
	}

	private void executeStatusGoingToMine(Unit self) {
		// Estado onde o agente vai caminhando para a mina

		Resource res = (Resource) this.getMemory().get(this.bestResource);
		int selfX = self.getX();
		int selfY = self.getY();

		int resX = res.getX();
		int resY = res.getY();

		double dist = this.distance(selfX, selfY, resX, resY);

		// Caso a distancia entre a mina e o agente seja suficiente para ele
		// coletar -> colete
		// Caso contrario continue andando
		if (dist <= self.getCollectRange()) {
			this.status = Status.COLLECTING;
		} else if (resourcesMark.get(bestResource) > 0) {
			if (this.nextPoint.getX() != selfX || this.nextPoint.getY() != selfY) {
				this.move(this.nextPoint.getX(), this.nextPoint.getY(), self.getSpeed());
			} else {
				goToPlace(self, res, selfX, selfY, resX, resY, Status.GOING_TO_MINE);
			}
		} else {
			this.status = Status.DECIDING_MINE;
		}
	}

	private void executeStatusDecidingMine(Unit self) {
		// Estado onde o agente decide qual a melhor mina para ir (mais
		// proxima)

		double minDist = Double.MAX_VALUE;
		boolean exist = false;

		Point[] bestPath = null;

		for (int r = 0; r < resources.length; r++) {
			Resource res = resources[r];
			if (this.resourcesMark.get(res.getId()) > 0) {// Verificar o
				// blackboard.
				StaticObstacle so = new StaticObstacle(res.getX(), res.getY(), res.getWidth(), res.getHeight());
				this.tiles.removeObstacle(so);
				List<Point> path = this.tiles.aStar(res.getX(), res.getY(), self.getX(), self.getY());
				if (!path.isEmpty()) {
					int dist = path.size();
					if (dist < minDist) {
						minDist = dist;

						this.bestResource = res.getId();
						this.nextPoint = path.get(0);
						bestPath = path.toArray(new Point[0]);
						exist = true;
					}
				}
				this.tiles.addObstacle(so);
			}
		}

		// Caso tal melhor mina exista va para a mina, caso contrario FIM
		if (exist) {

			processPoints(bestPath);

			this.status = Status.GOING_TO_MINE;
		} else {
			this.prevStatus = Status.DECIDING_MINE;
			this.status = Status.WAITING;
		}
	}

	private void processPoints(Point[] bestPath) {
		points.clear();
		int i = 0;
		int factor = bestPath.length / 5;
		while (i < 5 && i < bestPath.length) {
			points.add(bestPath[i * factor]);
		}
		if (i * factor != bestPath.length - 1) {
			points.add(bestPath[bestPath.length - 1]);
		}
	}

	private void goToPlace(Unit self, Entity ent, int selfX, int selfY, int entX, int entY, Status previousStatus) {
		StaticObstacle so;

		Point nextCheckPoint = this.points.get(0);
		for (int i = 0; i < this.units.length; i++) {
			if (getTile((Unit) this.getMemory().get(units[i])).equals(nextCheckPoint)) {
				points.remove(0);
				nextCheckPoint = points.get(0);
				break;
			}
		}

		if (ent instanceof Resource) {
			Resource res = (Resource) ent;
			so = new StaticObstacle(entX, entY, res.getWidth(), res.getHeight());
		} else {
			Building bui = (Building) ent;
			so = new StaticObstacle(entX, entY, bui.getWidth(), bui.getHeight());
		}

		this.tiles.removeObstacle(so);
		List<Point> path = this.tiles.aStar(so.getX(), so.getY(), selfX, selfY);
		this.tiles.addObstacle(so);
		if (path.isEmpty()) {
			this.status = Status.WAITING;
			this.prevStatus = previousStatus;
		} else {
			this.nextPoint = path.get(0);
			this.move(this.nextPoint.getX(), this.nextPoint.getY(), self.getSpeed());
		}
	}

	/**
	 * Calcula a distancia euclideana entre p1 e p2
	 * 
	 * @param p1
	 *            - Ponto 1
	 * @param p2
	 *            - Ponto 2
	 * @return A distancia euclideana entre p1 e p2
	 */
	private double distance(int p1X, int p1Y, int p2X, int p2Y) {
		return Math.sqrt(Math.pow((p1X - p2X), 2) + Math.pow((p1Y - p2Y), 2));
	}

	/**
	 * Metodos senseChange, senseEntity e clone sao override, o clone eh
	 * obrigatorio desta forma. Creio que nao seja necessario implementar os
	 * outros.
	 */
	public void senseChange(Entity entity) {
	}

	public void senseEntity(Entity oldEntity, Entity newEntity) {
	}

	private Point getTile(Unit unit) {
		Point ret;

		ret = tiles.getCentral(this.tiles.getTile(unit.getX(), unit.getY()));

		return ret;
	}

	public Agent clone() {
		CopyOfMinerAgent result = new CopyOfMinerAgent(this.getName(), this.getTeam());
		result.setActive(this.isActive());
		result.setPolicy(this.getPolicy());
		return result;
	}

}